// --------------------------------------------------------------------------------------------------------------------
// <copyright file="AgentHost.cs" company="Microsoft Corporation">
// The MIT License (MIT)
// 
// Copyright (c) 2014, Microsoft Corporation
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
namespace Microsoft.Robotics.Runtime
{
    using System;
    using System.Collections.Generic;
    using System.Reflection;

    /// <summary>
    /// Internal container class that wraps an agent to hide the details of the agent wiring.
    /// An instance of this class is create for each agent instance registered with a Locator. 
    /// The host extracts the agent metadata needed to wire the agent to other agents.
    /// The host also runs the control loop that executes the agent code. 
    /// The host ensures that the subscription and control methods are serialized, so that the agent doesn't have to worry about concurrency. 
    /// </summary>
    internal class AgentHost : PipelineElement
    {
        /// <summary>
        /// The agent wrapped by this instance.
        /// </summary>
        private IAgent wrappedAgent;

        /// <summary>
        /// Initializes a new instance of the AgentHost class. 
        /// </summary>
        /// <param name="wrappedAgent">The agent to wrap.</param>
        /// <param name="locator">The locator to use for agent wiring.</param>
        internal AgentHost(IAgent wrappedAgent, AgentLocator locator)
            : base(locator)
        {
            this.wrappedAgent = wrappedAgent;
        }

        /// <summary>
        /// Gets the agent we are wrapping.
        /// </summary>
        internal IAgent WrappedAgent
        {
            get
            {
                return this.wrappedAgent;
            }
        }

        /// <summary>
        /// Gets the name of the wrapped agent instance.
        /// </summary>
        internal override string Name
        {
            get
            {
                return this.wrappedAgent.Name;
            }
        }

        /// <summary>
        /// Extracts the relevant metadata from the inner agent (subscription pins, control pins, publishing pins).
        /// </summary>
        /// <param name="offlineMode">
        /// If false (default), receiver and control ports drop old messages when full (unless a different behavior is specified via attributes).
        /// If true, all receiver and control ports are set to block when full.
        /// </param>
        /// <param name="pipelineCreationTime">The time when the locator was created. Used as the time origin for agent message timestamps</param>
        internal override void InitializePorts(bool offlineMode, long pipelineCreationTime)
        {
            base.InitializePorts(offlineMode, pipelineCreationTime);
            Type agentType = this.wrappedAgent.GetType();
            Type[] interfaces = agentType.GetInterfaces();

            // get publishing, control and subscription pins
            Type publisherPrototype = typeof(IMessagePublisher<>);
            Type untypedPublisherPrototype = typeof(IMessagePublisher);
            Type subscriberPrototype = typeof(ISubscriptionReceiver<>);
            Type untypedSubscriberPrototype = typeof(ISubscriptionReceiver);
            Type controlPrototype = typeof(IControlReceiver<>);
            Type untypedControlPrototype = typeof(IControlReceiver);
            foreach (Type iface in interfaces)
            {
                if (!iface.IsGenericType)
                {
                    // look for the weak-typed interfaces
                    if (iface == untypedPublisherPrototype)
                    {
                        IMessagePublisher publisher = (IMessagePublisher)this.wrappedAgent;
                        Dictionary<Type, IForwarder> publishingPorts = new Dictionary<Type, IForwarder>();
                        foreach (Type msgType in publisher.PublishedTypes)
                        {
                            IBroadcastForwarder port = this.AddPublishingPort(msgType);
                            publishingPorts.Add(msgType, port);
                        }

                        publisher.InitializePublishers(publishingPorts);
                    }
                    else if (iface == untypedSubscriberPrototype)
                    {
                        ISubscriptionReceiver subscriber = (ISubscriptionReceiver)this.wrappedAgent;
                        ReceiverConfigurationAttribute config = this.GetReceiverConfiguration("Receive", typeof(AgentMessage));
                        bool blockIfFull = this.ShouldReceiverBlockIfFull(offlineMode, config);

                        foreach (Type msgType in subscriber.SubscriptionTypes)
                        {
                            this.AddSubscriptionPort(msgType, subscriber, blockIfFull, config.QueueSize);
                        }
                    }
                    else if (iface == untypedControlPrototype)
                    {
                        IControlReceiver controllable = (IControlReceiver)this.wrappedAgent;
                        ReceiverConfigurationAttribute config = this.GetReceiverConfiguration("ReceiveControl", typeof(AgentMessage));
                        bool blockIfFull = this.ShouldReceiverBlockIfFull(offlineMode, config);

                        foreach (Type msgType in controllable.ControlTypes)
                        {
                            this.AddControlPort(msgType, controllable, blockIfFull, config.QueueSize);
                        }
                    }
                }
                else
                {
                    // look for the strong-typed interfaces
                    Type basetype = iface.GetGenericTypeDefinition();
                    Type[] genericArgs = iface.GetGenericArguments();
                    Type messageType = genericArgs[0];

                    if (basetype == publisherPrototype)
                    {
                        IBroadcastForwarder forwarder = this.AddPublishingPort(messageType);

                        // pass the forwarder back to the agent
                        iface.InvokeMember(
                            "InitializePublisher",
                            BindingFlags.InvokeMethod,
                            null,
                            this.wrappedAgent,
                            new object[] { forwarder });
                    }
                    else if (basetype == subscriberPrototype)
                    {
                        ReceiverConfigurationAttribute config = this.GetReceiverConfiguration("Receive", messageType);
                        bool blockIfFull = this.ShouldReceiverBlockIfFull(offlineMode, config);
                        this.AddSubscriptionPort(messageType, this.wrappedAgent, blockIfFull, config.QueueSize);
                    }
                    else if (basetype == controlPrototype)
                    {
                        ReceiverConfigurationAttribute config = this.GetReceiverConfiguration("ReceiveControl", messageType);
                        bool blockIfFull = this.ShouldReceiverBlockIfFull(offlineMode, config);
                        this.AddControlPort(messageType, this.wrappedAgent, blockIfFull, config.QueueSize);
                    }
                }
            }
        }

        /// <summary>
        /// Called to do our own activation before the control loop is started
        /// </summary>
        protected override void OnActivated()
        {
            // tell the agent it's being activated. This usually results in a cascading activation of upstream agents.
            this.wrappedAgent.OnActivated();
        }

        /// <summary>
        /// Called to do our own cleanup right after the control loop is stopped. 
        /// </summary>
        protected override void OnDeactivated()
        {
            // tell the agent it's being deactivated. This usually results in a cascading deactivation of upstream agents.
            this.wrappedAgent.OnDeactivated();
        }

        /// <summary>
        /// Retrieves the receiver config attribute if one is present on the Receive method for the specified type
        /// </summary>
        /// <param name="methodName">The name of the method to look for.</param>
        /// <param name="messageType">The message type identifying which Receive method to look for</param>
        /// <returns>A receiver config attribute, or null if one is not present</returns>
        private ReceiverConfigurationAttribute GetReceiverConfiguration(string methodName, Type messageType)
        {
            // find the method 
            Type agentType = this.wrappedAgent.GetType();
            MethodInfo mi = agentType.GetMethod(methodName, new[] { messageType });
            ReceiverConfigurationAttribute config = mi.GetCustomAttribute<ReceiverConfigurationAttribute>();
            if (config == null)
            {
                config = new ReceiverConfigurationAttribute();
            }

            return config;
        }

        /// <summary>
        /// Determines whether the receiver should block additional messages once its full, or should drop older messages to make room for new ones.
        /// </summary>
        /// <param name="offlineMode">
        /// If false (default), receiver and control ports drop old messages when full (unless a different behavior is specified via attributes).
        /// If true, all receiver and control ports are set to block when full.
        /// </param>
        /// <param name="config">A receiver config attribute instance</param>
        /// <returns>True if the receiver should be configured to block, false otherwise</returns>
        private bool ShouldReceiverBlockIfFull(bool offlineMode, ReceiverConfigurationAttribute config)
        {
            return offlineMode || (config != null && config.BlockIfFull);
        }
    }
}
